Read Buf

Read Buf

Rust 中的惯用错误处理方式

错误处理是任何编程语言的关键部分,而 Rust 提供了一种独特而强大的错误处理方法。与许多其他编程语言不同,Rust 没有异常机制,而是提供了 Result 枚举,这迫使开发者以一致且可预测的方式处理所有错误,从而更容易识别和诊断问题。

由于 Rust 没有异常,每个函数要么返回一个值,要么 “panic”。当一个函数 panic 时,进程会立即退出并向调用者提供一些反馈。虽然在 Rust 中可以使用 catch_unwind 技术上可以捕获 panic,但这并不推荐用于一般用途。相反,Rust 提供了 Result 枚举,强制开发者处理所有错误。

本博文将探讨 Rust 中的惯用错误处理模式,帮助你理解其基础知识。我们将介绍 Result 枚举,它在 Rust 程序中如何用于处理错误,以及一些使这个过程更简单的常用 crate。

Result 类型

如果一个函数可能失败,那么它通常会返回 Rust 的 Result 类型。当从函数返回 Result 时,开发者必须返回其两个变体之一:Result::Ok 或 Result::Err。由于这些变体非常常见,它们在 prelude 中可用,所以你可以直接写 Ok 或 Err。

由于 Result 是一个枚举,所以可以很直接地匹配错误并处理任一情况:

fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
    if succeed {
        return Ok("success!");
    }
    Err("this is an error message")
}

fn main() -> Result<(), &'static str> {
    let result = fallible(false);
    let value = match result {
        Ok(value) => value,
        Err(err) => {
            return Err(err);
        }
    };

    println!("got a value: {value}");
    Ok(())
}

如你所见,我们可以使用 Rust 的标准 match 运算符来分支处理任一枚举变体。在 Rust 中,函数不会抛出异常,而是可以 panic (理想情况下永远不应发生)或返回 Result 类型。这个简单的例子总是返回一个错误,但你可以想象一个更复杂的函数可能会以某种意外的方式失败。

Rust 甚至允许 main 函数返回 Result。如果 main 返回的值是一个错误,Rust 将使用错误的 Debug 表示打印错误,并以错误代码退出进程。

问号运算符

当代码库大量使用 Result 时,处理每一个错误情况可能会变得繁琐。为了克服这个负担,Rust 提供了问号运算符,它是一种解包成功结果或将错误返回给调用者的简写方式。本质上,这个问号运算符将错误传播——或者说"冒泡"——给调用者。

例如,我们可以大大简化前面的例子:

fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
    if succeed {
        return Ok("success!");
    }
    Err("this is an error message")
}

fn main() -> Result<(), &'static str> {
    let value = fallible(false)?;
    println!("got a value: {value}");
    Ok(())
}

这个例子等同于第一个,但我们使用 ? 运算符来轻松地从 Result::Ok 中提取预期的值(如果存在);否则将 Result::Error 变体返回给调用者。

错误封装

在之前的例子中,我们使用简单的字符串来返回错误。在更复杂的情况下,你可能会遇到表示可以返回的不同类型错误的错误类型。

考虑以下示例:

use reqwest::blocking::get;

fn download() -> Result<String, reqwest::Error> {
    let website_text = get("https://www.rust-lang.org")?.text()?;
    Ok(website_text)
}

fn main() -> Result<(), reqwest::Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

此示例从 https://www.rust-lang.org 下载内容,我们在这里使用 ? 操作符来简化错误处理。注意,每个 Result 的第二个类型现在是 reqwest::Error。这种类型适用,因为 get() 和 text() 返回的 Result 的错误类型均为 reqwest::Error。

现在,假设你还想将下载的网站文本存储在文件中。我们可以轻松地更新代码来完成这一操作:

use tempfile::tempfile;
use std::io::copy;
use reqwest::blocking::get;

fn download() -> Result<String, reqwest::Error> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), reqwest::Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

但这段代码无法编译!编译上述代码会产生以下错误:

 |
5 | fn download() -> Result<String, reqwest::Error> {
  |                  ------------------------------ expected `reqwest::Error` because of this
6 |     let mut file = tempfile()?;
  |                              ^ the trait `From<std::io::Error>` is not implemented for `reqwest::Error`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `FromResidual<R>`:
            <Result<T, F> as FromResidual<Result<Infallible, E>>>
            <Result<T, F> as FromResidual<Yeet<E>>>
  = note: required for `Result<String, reqwest::Error>` to implement `FromResidual<Result<Infallible, std::io::Error>>`

这个错误发生是因为 tempfile() 返回的错误类型与我们的 Result<String, reqwest::Error> 函数签名不匹配。解决这个问题最简单的方法是“封装”错误,并返回 Result<String, Box>。

标准库提供了这个 std::error::Error trait 来处理需要返回多种错误类型的情况。库暴露的错误类型应实现 Error,这样我们就可以轻松地将几种不同的错误类型转换为通用的 trait 对象。再次使用 ? 操作符可以轻松实现这一点。

use reqwest::blocking::get;
use std::error::Error;
use std::io::copy;
use tempfile::tempfile;

fn download() -> Result<String, Box<dyn Error>> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), Box<dyn Error>> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

还值得一提的是 anyhow,这是另一个流行的 crate,提供了类似于使用 Box> trait 对象方法的体验,并增加了一些开发者友好的功能。

使用 thiserror

使用 anyhow 或 Box> 的缺点是我们失去了函数返回的错误类型,这些错误类型通常有助于编写基于发生的错误类型的分支逻辑。

维护错误上下文的常用约定是构建一个自定义错误枚举,描述在 crate 或模块中可能发生的错误。

use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;

fn download() -> Result<String, Error> {
    let mut file = tempfile().map_err(|_| Error::File)?;
    let website_text = get("https://www.rust-lang.org")
        .map_err(|_| Error::Download)?
        .text()
        .map_err(|_| Error::Download)?;
    copy(&mut website_text.as_bytes(), &mut file).map_err(|_| Error::File)?;
    Ok(website_text)
}

fn main() -> Result<(), Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

#[derive(Debug)]
enum Error {
    File,
    Download,
}

现在,调用者了解了下载函数中产生的错误类型。然而,我们需要手动使用 map_err 将错误转换为我们的自定义 Error 枚举,这再次变得繁琐且冗长。

thiserror 使将错误类型映射到您的自定义错误类型变得更加容易:

use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;
use thiserror::Error;

fn download() -> Result<String, Error> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

#[derive(Debug, Error)]
enum Error {
    #[error("file error: {0}")]
    File(#[from] std::io::Error),
    #[error("download error: {0}")]
    Download(#[from] reqwest::Error),
}

注意我们如何使用 thiserror 提供的宏声明性地保持错误映射到我们的自定义枚举,而我们的代码保持简洁,能够使用 ? 操作符简单地传播错误。

结论

错误处理在任何编程语言中都是至关重要的,Rust 的错误处理提供了一种强大的方法。与许多其他编程语言不同,Rust 根本不使用异常,而是依赖 Result 枚举强制开发者处理所有错误。Rust 错误处理的这种方法不仅更可靠和有效,而且也使开发更加愉快。Result 枚举使得理解和处理代码中的错误条件变得简单,这是 Rust 错误处理系统的一个主要优势。